import { ChangesObservable } from 'handsontable/translations/changesObservable/observable';
import { ChangesObserver } from 'handsontable/translations/changesObservable/observer';

describe('ChangesObservable', () => {
  it('should write to the subscribers that are not unsubscribed', () => {
    const observable = new ChangesObservable();
    const observer1 = observable.createObserver();
    const observer2 = observable.createObserver();

    const observer1Listener1 = jasmine.createSpy('observer1Listener1');
    const observer2Listener1 = jasmine.createSpy('observer2Listener1');
    const observer2Listener2 = jasmine.createSpy('observer2Listener2');

    observer1.subscribe(observer1Listener1);
    observer2.subscribe(observer2Listener1);
    observer2.subscribe(observer2Listener2);

    expect(observer1Listener1).toHaveBeenCalledTimes(0);
    expect(observer2Listener1).toHaveBeenCalledTimes(0);
    expect(observer2Listener2).toHaveBeenCalledTimes(0);

    observable.emit([false, true, false, true, true, false]);

    expect(observer1Listener1).toHaveBeenCalledTimes(1);
    expect(observer1Listener1).toHaveBeenLastCalledWith([
      { op: 'replace', index: 1, oldValue: false, newValue: true },
      { op: 'replace', index: 3, oldValue: false, newValue: true },
      { op: 'replace', index: 4, oldValue: false, newValue: true },
    ]);
    expect(observer2Listener1).toHaveBeenCalledTimes(1);
    expect(observer2Listener1).toHaveBeenLastCalledWith([
      { op: 'replace', index: 1, oldValue: false, newValue: true },
      { op: 'replace', index: 3, oldValue: false, newValue: true },
      { op: 'replace', index: 4, oldValue: false, newValue: true },
    ]);
    expect(observer2Listener2).toHaveBeenCalledTimes(1);
    expect(observer2Listener2).toHaveBeenLastCalledWith([
      { op: 'replace', index: 1, oldValue: false, newValue: true },
      { op: 'replace', index: 3, oldValue: false, newValue: true },
      { op: 'replace', index: 4, oldValue: false, newValue: true },
    ]);

    observer2.unsubscribe();
    observable.emit([false, false, true, true, true, false]);

    expect(observer1Listener1).toHaveBeenCalledTimes(2);
    expect(observer2Listener1).toHaveBeenCalledTimes(1);
    expect(observer2Listener2).toHaveBeenCalledTimes(1);
  });

  describe('constructor', () => {
    it('should be possible to change how the initial bulk changes are generated by changing "initialIndexValue" option', () => {
      const observable = new ChangesObservable({
        initialIndexValue: true,
      });

      observable.emit([false, true, false, true, true, false]);

      {
        const observer = observable.createObserver();
        const observerListener = jasmine.createSpy('observerListener');

        observer.subscribe(observerListener);

        expect(observerListener).toHaveBeenCalledTimes(1);
        expect(observerListener).toHaveBeenLastCalledWith([
          { op: 'replace', index: 0, oldValue: true, newValue: false },
          { op: 'replace', index: 2, oldValue: true, newValue: false },
          { op: 'replace', index: 5, oldValue: true, newValue: false },
        ]);
      }
    });
  });

  describe('createObserver', () => {
    it('should create observer', () => {
      const observable = new ChangesObservable();
      const observer = observable.createObserver();

      expect(observer).toBeInstanceOf(ChangesObserver);
    });

    it('should not emit any subscribe callbacks when there are no bulk initial changes', () => {
      const observable = new ChangesObservable();
      const observer = observable.createObserver();

      const observerListener = jasmine.createSpy('observerListener');

      observer.subscribe(observerListener);

      expect(observerListener).toHaveBeenCalledTimes(0);
    });
  });

  describe('emit', () => {
    it('should trigger all subscribers after emitting new changes with the same size', () => {
      const observable = new ChangesObservable();
      const observer1 = observable.createObserver();
      const observer2 = observable.createObserver();

      const observer1Listener1 = jasmine.createSpy('observer1Listener1');
      const observer2Listener1 = jasmine.createSpy('observer2Listener1');
      const observer2Listener2 = jasmine.createSpy('observer2Listener2');

      observer1.subscribe(observer1Listener1);
      observer2.subscribe(observer2Listener1);
      observer2.subscribe(observer2Listener2);

      expect(observer1Listener1).toHaveBeenCalledTimes(0);
      expect(observer2Listener1).toHaveBeenCalledTimes(0);
      expect(observer2Listener2).toHaveBeenCalledTimes(0);

      observable.emit([false, true, false, true, true, false]);

      expect(observer1Listener1).toHaveBeenCalledTimes(1);
      expect(observer1Listener1).toHaveBeenLastCalledWith([
        { op: 'replace', index: 1, oldValue: false, newValue: true },
        { op: 'replace', index: 3, oldValue: false, newValue: true },
        { op: 'replace', index: 4, oldValue: false, newValue: true },
      ]);
      expect(observer2Listener1).toHaveBeenCalledTimes(1);
      expect(observer2Listener1).toHaveBeenLastCalledWith([
        { op: 'replace', index: 1, oldValue: false, newValue: true },
        { op: 'replace', index: 3, oldValue: false, newValue: true },
        { op: 'replace', index: 4, oldValue: false, newValue: true },
      ]);
      expect(observer2Listener2).toHaveBeenCalledTimes(1);
      expect(observer2Listener2).toHaveBeenLastCalledWith([
        { op: 'replace', index: 1, oldValue: false, newValue: true },
        { op: 'replace', index: 3, oldValue: false, newValue: true },
        { op: 'replace', index: 4, oldValue: false, newValue: true },
      ]);

      observable.emit([true, true, false, true, true, false]);

      expect(observer1Listener1).toHaveBeenCalledTimes(2);
      expect(observer1Listener1).toHaveBeenLastCalledWith([
        { op: 'replace', index: 0, oldValue: false, newValue: true },
      ]);
      expect(observer2Listener1).toHaveBeenCalledTimes(2);
      expect(observer2Listener1).toHaveBeenLastCalledWith([
        { op: 'replace', index: 0, oldValue: false, newValue: true },
      ]);
      expect(observer2Listener2).toHaveBeenCalledTimes(2);
      expect(observer2Listener2).toHaveBeenLastCalledWith([
        { op: 'replace', index: 0, oldValue: false, newValue: true },
      ]);

      observable.emit([true, false, false, true, true, false]);

      expect(observer1Listener1).toHaveBeenCalledTimes(3);
      expect(observer1Listener1).toHaveBeenLastCalledWith([
        { op: 'replace', index: 1, oldValue: true, newValue: false },
      ]);
      expect(observer2Listener1).toHaveBeenCalledTimes(3);
      expect(observer2Listener1).toHaveBeenLastCalledWith([
        { op: 'replace', index: 1, oldValue: true, newValue: false },
      ]);
      expect(observer2Listener2).toHaveBeenCalledTimes(3);
      expect(observer2Listener2).toHaveBeenLastCalledWith([
        { op: 'replace', index: 1, oldValue: true, newValue: false },
      ]);
    });

    it('should trigger all subscribers after emitting new changes that contain more indexes than in the previous call', () => {
      const observable = new ChangesObservable();
      const observer = observable.createObserver();

      const observerListener = jasmine.createSpy('observerListener');

      observer.subscribe(observerListener);

      observable.emit([false, true, false, true]);

      expect(observerListener).toHaveBeenCalledTimes(1);
      expect(observerListener).toHaveBeenLastCalledWith([
        { op: 'replace', index: 1, oldValue: false, newValue: true },
        { op: 'replace', index: 3, oldValue: false, newValue: true },
      ]);

      // The case emulates the situation where insert indexes are performed
      //                           ↓new indexes↓
      observable.emit([false, true, false, false, false, true]);

      expect(observerListener).toHaveBeenCalledTimes(2);
      expect(observerListener).toHaveBeenLastCalledWith([
        { op: 'replace', index: 3, oldValue: true, newValue: false },
        { op: 'insert', index: 4, oldValue: undefined, newValue: false },
        { op: 'insert', index: 5, oldValue: undefined, newValue: true },
      ]);
    });

    it('should trigger all subscribers after emitting new changes that contain less indexes than in the previous call', () => {
      const observable = new ChangesObservable();
      const observer = observable.createObserver();

      const observerListener = jasmine.createSpy('observerListener');

      observer.subscribe(observerListener);

      observable.emit([false, true, false, true, true]);

      expect(observerListener).toHaveBeenCalledTimes(1);
      expect(observerListener).toHaveBeenLastCalledWith([
        { op: 'replace', index: 1, oldValue: false, newValue: true },
        { op: 'replace', index: 3, oldValue: false, newValue: true },
        { op: 'replace', index: 4, oldValue: false, newValue: true },
      ]);

      // The case emulates the situation where remove indexes are performed
      observable.emit([false, false, true]);

      expect(observerListener).toHaveBeenCalledTimes(2);
      expect(observerListener).toHaveBeenLastCalledWith([
        { op: 'replace', index: 1, oldValue: true, newValue: false },
        { op: 'replace', index: 2, oldValue: false, newValue: true },
        { op: 'remove', index: 3, oldValue: true, newValue: undefined },
        { op: 'remove', index: 4, oldValue: true, newValue: undefined },
      ]);
    });

    it('should emit the changes when the passed new index state is an empty array', () => {
      const observable = new ChangesObservable();
      const observer = observable.createObserver();

      const observerListener = jasmine.createSpy('observerListener');

      observer.subscribe(observerListener);

      observable.emit([false, true, false, true, true]);

      expect(observerListener).toHaveBeenCalledTimes(1);

      // The case emulates the situation when the index map collection returns an empty array.
      // It happens when the last index map is unregistered form the collection or the passed
      // dataset is empty.
      observable.emit([]);

      expect(observerListener).toHaveBeenCalledTimes(2);
      expect(observerListener).toHaveBeenLastCalledWith([
        { op: 'replace', index: 1, oldValue: true, newValue: false },
        { op: 'replace', index: 3, oldValue: true, newValue: false },
        { op: 'replace', index: 4, oldValue: true, newValue: false },
      ]);
    });

    it('should trigger all new subscribers with initial bulk changes', () => {
      const observable = new ChangesObservable();

      observable.emit([false, true, false, true, true, false]);

      {
        const observer = observable.createObserver();
        const observerListener = jasmine.createSpy('observerListener');

        observer.subscribe(observerListener);

        expect(observerListener).toHaveBeenCalledTimes(1);
        expect(observerListener).toHaveBeenLastCalledWith([
          { op: 'replace', index: 1, oldValue: false, newValue: true },
          { op: 'replace', index: 3, oldValue: false, newValue: true },
          { op: 'replace', index: 4, oldValue: false, newValue: true },
        ]);
      }

      observable.emit([false, false, false, true, true, false]);
      observable.emit([true, false, false, true, false, false]);
      observable.emit([false, false, false, true, false, false]);

      {
        const observer = observable.createObserver();
        const observerListener = jasmine.createSpy('observerListener');

        observer.subscribe(observerListener);

        expect(observerListener).toHaveBeenCalledTimes(1);
        expect(observerListener).toHaveBeenLastCalledWith([
          { op: 'replace', index: 3, oldValue: false, newValue: true },
        ]);
      }

      observable.emit([false, false, false, false, false]);
      observable.emit([false, true, false]);

      {
        const observer = observable.createObserver();
        const observerListener = jasmine.createSpy('observerListener');

        observer.subscribe(observerListener);

        expect(observerListener).toHaveBeenCalledTimes(1);
        expect(observerListener).toHaveBeenLastCalledWith([
          { op: 'replace', index: 1, oldValue: false, newValue: true },
        ]);
      }

      observable.emit([false, true, false]);
      observable.emit([true, false, false, false, true, false, false, true]);

      {
        const observer = observable.createObserver();
        const observerListener = jasmine.createSpy('observerListener');

        observer.subscribe(observerListener);

        expect(observerListener).toHaveBeenCalledTimes(1);
        expect(observerListener).toHaveBeenLastCalledWith([
          { op: 'replace', index: 0, oldValue: false, newValue: true },
          { op: 'replace', index: 4, oldValue: false, newValue: true },
          { op: 'replace', index: 7, oldValue: false, newValue: true },
        ]);
      }
    });
  });
});
